package com.website.bookshelf.service;

import com.website.bookshelf.model.dto.AuthenticationTokenDto;
import com.website.bookshelf.model.dto.UserCreationDto;
import com.website.bookshelf.model.dto.UserCredentialsDto;
import com.website.bookshelf.model.entity.User;
import com.website.bookshelf.model.type.Role;
import com.website.bookshelf.repository.UserRepository;
import jakarta.mail.MessagingException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import java.time.Instant;
import java.util.Set;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {
    private static final long EXPIRATION_TIME = 36000L;
    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private final JwtEncoder jwtEncoder;
    private final EmailService emailService;

    public User findByEmail(String email) {
        return userRepository.findByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException("User with email \"" + email + "\" was not found!"));
    }

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = this.findByEmail(email);

        return org.springframework.security.core.userdetails.User
                .withUsername(user.getEmail())
                .password(user.getPassword())
                .authorities(user.getRoles())
                .accountLocked(user.getVerified())
                .build();
    }

    public UserCreationDto createUserByEmail(UserCredentialsDto userCredentialsDto) throws MessagingException {
        if (userRepository.findByEmail(userCredentialsDto.getEmail()).isPresent()) {
            throw new ResponseStatusException(HttpStatus.IM_USED, "User with email \"" + userCredentialsDto.getEmail() + "\" already exists!");
        }


        User user = User.builder()
                .email(userCredentialsDto.getEmail())
                .password(bCryptPasswordEncoder.encode(userCredentialsDto.getPassword()))
                .roles(Set.of(Role.USER))
                .verified(false)
                .build();

        User savedUser = userRepository.save(user);
        emailService.sendVerificationEmail(savedUser.getEmail());

        return UserCreationDto.builder()
                .email(savedUser.getEmail())
                .createdAt(savedUser.getCreatedAt())
                .build();
    }

    public AuthenticationTokenDto login(Authentication authentication) {
        Instant now = Instant.now();
        String scope = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(" "));

        JwtClaimsSet claims = JwtClaimsSet.builder()
                .issuer("self")
                .issuedAt(now)
                .expiresAt(now.plusSeconds(EXPIRATION_TIME))
                .subject(authentication.getName())
                .claim("scope", scope)
                .build();

        return AuthenticationTokenDto.builder()
                .token(this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue())
                .username(authentication.getName())
                .roles('[' + claims.getClaim("scope").toString().replace(" ", ", ") + ']')
                .expiresAt(claims.getIssuedAt())
                .issuedAt(claims.getIssuedAt())
                .build();
    }
}
